#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""docstring
"""

__author__ = "Gu Yingbo (tensiongyb@gmail.com)"

import base64
import asyncore
import asynchat
import os
import re
quoted_slash = re.compile("(?i)%2F")
import rfc822
import socket
try:
    import cStringIO as StringIO
except ImportError:
    import StringIO
import sys
import signal
from traceback import format_exc
from urllib import unquote, splitquery
from urlparse import urlparse


comma_separated_headers = ['ACCEPT', 'ACCEPT-CHARSET', 'ACCEPT-ENCODING',
    'ACCEPT-LANGUAGE', 'ACCEPT-RANGES', 'ALLOW', 'CACHE-CONTROL',
    'CONNECTION', 'CONTENT-ENCODING', 'CONTENT-LANGUAGE', 'EXPECT',
    'IF-MATCH', 'IF-NONE-MATCH', 'PRAGMA', 'PROXY-AUTHENTICATE', 'TE',
    'TRAILER', 'TRANSFER-ENCODING', 'UPGRADE', 'VARY', 'VIA', 'WARNING',
    'WWW-AUTHENTICATE']

class RequestHandler(asynchat.async_chat):

    def __init__(self, sock, server):
        asynchat.async_chat.__init__(self, sock)
        self.server = server
        self.reset()

    def reset(self):
        self.ac_in_buffer = ''
        self.ac_out_buffer = ''
        self.producer_fifo = asynchat.fifo()
        self.set_terminator("\r\n\r\n")
        self.process = "reading headers"
        self.started_response = False
        self.status = ""
        self.outheaders = []
        self.sent_headers = False
        self.close_connection = False
        self.chunked_write = False
    
        self.rfile = StringIO.StringIO()

        self.environ = {
                "wsgi.version": (1, 0),
                "wsgi.url_scheme": "http",
                "wsgi.multithread": False,
                "wsgi.multiprocess": False,
                "wsgi.run_once": False,
                "wsgi.errors": sys.stderr,
        }

    def collect_incoming_data(self, data):
        """Buffer the data"""
        if self.process == "reading chunked":
            self.line_buffer += data
        else:
            self.rfile.write(data)

    def found_terminator(self):
        if self.process == "reading headers":
            self.rfile.write("\r\n\r\n")
            self.rfile.seek(0)
            try:
                self.parse_request()
            except:
                self.simple_response("500 Internal Server Error", format_exc())
                return

            if not self.read_chunked:
                if self.environ["REQUEST_METHOD"].upper() == "POST":
                    cl = int(self.environ["CONTENT_LENGTH"])
                    self.rfile = StringIO.StringIO()
                    self.environ["wsgi.input"] = self.rfile
                    self.process = "dealing post"
                    self.set_terminator(cl)
                else:
                    return self.wsgi_response()
            else:
                self.rfile = StringIO.StringIO()
                self.environ["wsgi.input"] = self.rfile
                self.process = "reading chunked"
                self.set_terminator("\r\n")
                self.line_buffer = ""
                self.cl = 0
        elif self.process == "dealing post":
            self.rfile.seek(0)
            return self.wsgi_response()
        elif self.process == "reading chunked":
            line = self.line_buffer.split(";", 1)
            chunk_size = int(line.pop(0), 16)
            if chunk_size <= 0:
                self.process = "reading tail headers"
                self.set_terminator("\r\n\r\n")
            else:
                self.cl += chunk_size
                self.process = "get chunked"
                self.set_terminator(self.cl+2) # browsers sometimes over-send
        elif self.process == "get chunked":
            crlf = self.rfile.getvalue()[-2:]
            if not crlf != "\r\n":
                self.simple_response("400 Bad Request",
                        "Bad chunked transfer coding "
                        "(expected '\\r\\n', got %r)" % crlf)
                return
            else:
                self.process = "reading chunked"
                self.set_terminator("\r\n")
                self.rfile.seek(-2, 2)
                self.rfile.truncate()
        elif self.process == "reading tail headers":
            self.read_headers()
            self.rfile.seek(0)
            self.environ["CONTENT_LENGTH"] = str(self.cl) or ""
            return self.wsgi_response()

    def reset_when_done(self):
        self.producer_fifo.push(0)

    def refill_buffer (self):
        while 1:
            if len(self.producer_fifo):
                p = self.producer_fifo.first()
                # a 'None' in the producer fifo is a sentinel,
                # telling us to close the channel.
                if p == 0:
                    if not self.ac_out_buffer:
                        self.producer_fifo.pop()
                        self.reset()
                    return
                elif p is None:
                    if not self.ac_out_buffer:
                        self.producer_fifo.pop()
                        self.close()
                    return
                elif isinstance(p, str):
                    self.producer_fifo.pop()
                    self.ac_out_buffer = self.ac_out_buffer + p 
                    return
                data = p.more()
                if data:
                    self.ac_out_buffer = self.ac_out_buffer + data
                    return
                else:
                    self.producer_fifo.pop()
            else:
                return

    def wsgi_response(self):
        try:
            self.respond()
        except:
            self.simple_response("500 Internal Server Error", format_exc())
            return

        if self.close_connection or ( not self.server.keep_going ):
            self.close_when_done()
        else:
            self.reset_when_done()

    def readable(self):
        return self.server.keep_going and not self.writable() and asynchat.async_chat.readable(self) 

    def handle_expt(self):
        asynchat.async_chat.handle_expt(self)
        self.close()

    def handle_error(self):
        asynchat.async_chat.handle_error(self)
        self.close()

    def parse_request(self):
        """Parse the next HTTP request start-line and message-headers."""
        # HTTP/1.1 connections are persistent by default. If a client
        # requests a page, then idles (leaves the connection open),
        # then rfile.readline() will raise socket.error("timed out").
        # Note that it does this based on the value given to settimeout(),
        # and doesn't need the client to request or acknowledge the close
        # (although your TCP stack might suffer for it: cf Apache's history
        # with FIN_WAIT_2).
        request_line = self.rfile.readline()
        if not request_line:
            # Force self.ready = False so the connection will close.
            self.ready = False
            return
        
        if request_line == "\r\n":
            # RFC 2616 sec 4.1: "...if the server is reading the protocol
            # stream at the beginning of a message and receives a CRLF
            # first, it should ignore the CRLF."
            # But only ignore one leading line! else we enable a DoS.
            request_line = self.rfile.readline()
            if not request_line:
                self.ready = False
                return
        
        server = self.server
        environ = self.environ
        environ["SERVER_SOFTWARE"] = "%s WSGI Server" % server.version
        
        method, path, req_protocol = request_line.strip().split(" ", 2)
        environ["REQUEST_METHOD"] = method
        
        # path may be an abs_path (including "http://host.domain.tld");
        scheme, location, path, params, qs, frag = urlparse(path)
        
        if frag:
            self.simple_response("400 Bad Request",
                                 "Illegal #fragment in Request-URI.")
            return
        
        if scheme:
            environ["wsgi.url_scheme"] = scheme
        if params:
            path = path + ";" + params
        
        # Unquote the path+params (e.g. "/this%20path" -> "this path").
        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
        #
        # But note that "...a URI must be separated into its components
        # before the escaped characters within those components can be
        # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2
        atoms = [unquote(x) for x in quoted_slash.split(path)]
        path = "%2F".join(atoms)
        
        environ["SCRIPT_NAME"] = ""
        environ["PATH_INFO"] = path
        ##if path == "*":
        ##    # This means, of course, that the last wsgi_app (shortest path)
        ##    # will always handle a URI of "*".
        ##    environ["SCRIPT_NAME"] = ""
        ##    environ["PATH_INFO"] = "*"
        ##    self.wsgi_app = server.mount_points[-1][1]
        ##else:
        ##    for mount_point, wsgi_app in server.mount_points:
        ##        # The mount_points list should be sorted by length, descending.
        ##        if path.startswith(mount_point + "/") or path == mount_point:
        ##            environ["SCRIPT_NAME"] = mount_point
        ##            environ["PATH_INFO"] = path[len(mount_point):]
        ##            self.wsgi_app = wsgi_app
        ##            break
        ##    else:
        ##        self.simple_response("404 Not Found")
        ##        return
        
        # Note that, like wsgiref and most other WSGI servers,
        # we unquote the path but not the query string.
        environ["QUERY_STRING"] = qs
        
        # Compare request and server HTTP protocol versions, in case our
        # server does not support the requested protocol. Limit our output
        # to min(req, server). We want the following output:
        #     request    server     actual written   supported response
        #     protocol   protocol  response protocol    feature set
        # a     1.0        1.0           1.0                1.0
        # b     1.0        1.1           1.1                1.0
        # c     1.1        1.0           1.0                1.0
        # d     1.1        1.1           1.1                1.1
        # Notice that, in (b), the response will be "HTTP/1.1" even though
        # the client only understands 1.0. RFC 2616 10.5.6 says we should
        # only return 505 if the _major_ version is different.
        rp = int(req_protocol[5]), int(req_protocol[7])
        sp = int(server.protocol[5]), int(server.protocol[7])
        if sp[0] != rp[0]:
            self.simple_response("505 HTTP Version Not Supported")
            return
        # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol.
        environ["SERVER_PROTOCOL"] = req_protocol
        # set a non-standard environ entry so the WSGI app can know what
        # the *real* server protocol is (and what features to support).
        # See http://www.faqs.org/rfcs/rfc2145.html.
        environ["ACTUAL_SERVER_PROTOCOL"] = server.protocol
        self.response_protocol = "HTTP/%s.%s" % min(rp, sp)
        
        # If the Request-URI was an absoluteURI, use its location atom.
        if location:
            environ["SERVER_NAME"] = location
        
        # then all the http headers
        try:
            self.read_headers()
        except ValueError, ex:
            self.simple_response("400 Bad Request", repr(ex.args))
            return
        
        creds = environ.get("HTTP_AUTHORIZATION", "").split(" ", 1)
        environ["AUTH_TYPE"] = creds[0]
        if creds[0].lower() == 'basic':
            user, pw = base64.decodestring(creds[1]).split(":", 1)
            environ["REMOTE_USER"] = user
        
        # Persistent connection support
        if self.response_protocol == "HTTP/1.1":
            if environ.get("HTTP_CONNECTION", "") == "close":
                self.close_connection = True
        else:
            # HTTP/1.0
            if environ.get("HTTP_CONNECTION", "") != "Keep-Alive":
                self.close_connection = True
        
        # Transfer-Encoding support
        te = None
        if self.response_protocol == "HTTP/1.1":
            te = environ.get("HTTP_TRANSFER_ENCODING")
            if te:
                te = [x.strip().lower() for x in te.split(",") if x.strip()]
        
        read_chunked = False
        
        if te:
            for enc in te:
                if enc == "chunked":
                    read_chunked = True
                else:
                    # Note that, even if we see "chunked", we must reject
                    # if there is an extension we don't recognize.
                    self.simple_response("501 Unimplemented")
                    self.close_connection = True
                    return
        
        self.read_chunked = read_chunked
        return

        if read_chunked:
            if not self.decode_chunked():
                return
        
        # From PEP 333:
        # "Servers and gateways that implement HTTP 1.1 must provide
        # transparent support for HTTP 1.1's "expect/continue" mechanism.
        # This may be done in any of several ways:
        #   1. Respond to requests containing an Expect: 100-continue request
        #      with an immediate "100 Continue" response, and proceed normally.
        #   2. Proceed with the request normally, but provide the application
        #      with a wsgi.input stream that will send the "100 Continue"
        #      response if/when the application first attempts to read from
        #      the input stream. The read request must then remain blocked
        #      until the client responds.
        #   3. Wait until the client decides that the server does not support
        #      expect/continue, and sends the request body on its own.
        #      (This is suboptimal, and is not recommended.)
        #
        # We used to do 3, but are now doing 1. Maybe we'll do 2 someday,
        # but it seems like it would be a big slowdown for such a rare case.
        if environ.get("HTTP_EXPECT", "") == "100-continue":
            self.simple_response(100)
        
        self.ready = True
    
    def read_headers(self):
        """Read header lines from the incoming stream."""
        environ = self.environ
        
        while True:
            line = self.rfile.readline()
            if not line:
                # No more data--illegal end of headers
                raise ValueError("Illegal end of headers.")
            
            if line == '\r\n':
                # Normal end of headers
                break
            
            if line[0] in ' \t':
                # It's a continuation line.
                v = line.strip()
            else:
                k, v = line.split(":", 1)
                k, v = k.strip().upper(), v.strip()
                envname = "HTTP_" + k.replace("-", "_")
            
            if k in comma_separated_headers:
                existing = environ.get(envname)
                if existing:
                    v = ", ".join((existing, v))
            environ[envname] = v
        
        ct = environ.pop("HTTP_CONTENT_TYPE", None)
        if ct:
            environ["CONTENT_TYPE"] = ct
        cl = environ.pop("HTTP_CONTENT_LENGTH", None)
        if cl:
            environ["CONTENT_LENGTH"] = cl
    
    def decode_chunked(self):
        """Decode the 'chunked' transfer coding."""
        cl = 0
        data = StringIO.StringIO()
        while True:
            line = self.rfile.readline().strip().split(";", 1)
            chunk_size = int(line.pop(0), 16)
            if chunk_size <= 0:
                break
##            if line: chunk_extension = line[0]
            cl += chunk_size
            data.write(self.rfile.read(chunk_size))
            crlf = self.rfile.read(2)
            if crlf != "\r\n":
                self.simple_response("400 Bad Request",
                                     "Bad chunked transfer coding "
                                     "(expected '\\r\\n', got %r)" % crlf)
                return
        
        # Grab any trailer headers
        self.read_headers()
        
        data.seek(0)
        self.environ["wsgi.input"] = data
        self.environ["CONTENT_LENGTH"] = str(cl) or ""
        return True
    
    def respond(self):
        """Call the appropriate WSGI app and write its iterable output."""
        response = self.server.wsgi_app(self.environ, self.start_response)
        try:
            for chunk in response:
                # "The start_response callable must not actually transmit
                # the response headers. Instead, it must store them for the
                # server or gateway to transmit only after the first
                # iteration of the application return value that yields
                # a NON-EMPTY string, or upon the application's first
                # invocation of the write() callable." (PEP 333)
                if chunk:
                    self.write(chunk)
        finally:
            if hasattr(response, "close"):
                response.close()

        if not self.sent_headers:
            self.send_headers()
        if self.chunked_write:
            self.push("0\r\n\r\n")
    
    def simple_response(self, status, msg=""):
        """Write a simple response back to the client."""
        status = str(status)
        buf = ["%s %s\r\n" % (self.server.protocol, status),
               "Content-Length: %s\r\n" % len(msg)]
        
        if status[:3] == "413" and self.response_protocol == 'HTTP/1.1':
            # Request Entity Too Large
            self.close_connection = True
            buf.append("Connection: close\r\n")
        
        buf.append("\r\n")
        if msg:
            buf.append(msg)
        self.push("".join(buf))
    
        if self.close_connection or ( not self.server.keep_going ):
            self.close_when_done()
        else:
            self.reset_when_done()

    def start_response(self, status, headers, exc_info = None):
        """WSGI callable to begin the HTTP response."""
        if self.started_response:
            if not exc_info:
                raise AssertionError("WSGI start_response called a second "
                                     "time with no exc_info.")
            else:
                try:
                    raise exc_info[0], exc_info[1], exc_info[2]
                finally:
                    exc_info = None
        self.started_response = True
        self.status = status
        self.outheaders.extend(headers)
        return self.write
    
    def write(self, chunk):
        """WSGI callable to write unbuffered data to the client.
        
        This method is also used internally by start_response (to write
        data from the iterable returned by the WSGI application).
        """
        if not self.started_response:
            raise AssertionError("WSGI write called before start_response.")
        
        if not self.sent_headers:
            self.send_headers()
        
        if self.chunked_write and chunk:
            buf = [hex(len(chunk))[2:], "\r\n", chunk, "\r\n"]
            self.push("".join(buf))
        else:
            self.push(chunk)
    
    def send_headers(self):
        """Assert, process, and send the HTTP response message-headers."""
        self.sent_headers = True
        hkeys = [key.lower() for key, value in self.outheaders]
        status = int(self.status[:3])
        
        if status == 413:
            # Request Entity Too Large. Close conn to avoid garbage.
            self.close_connection = True
        elif "content-length" not in hkeys:
            # "All 1xx (informational), 204 (no content),
            # and 304 (not modified) responses MUST NOT
            # include a message-body." So no point chunking.
            if status < 200 or status in (204, 205, 304):
                pass
            else:
                if self.response_protocol == 'HTTP/1.1':
                    # Use the chunked transfer-coding
                    self.chunked_write = True
                    self.outheaders.append(("Transfer-Encoding", "chunked"))
                else:
                    # Closing the conn is the only way to determine len.
                    self.close_connection = True
        
        if "connection" not in hkeys:
            if self.response_protocol == 'HTTP/1.1':
                if self.close_connection:
                    self.outheaders.append(("Connection", "close"))
            else:
                if not self.close_connection:
                    self.outheaders.append(("Connection", "Keep-Alive"))
        
        if "date" not in hkeys:
            self.outheaders.append(("Date", rfc822.formatdate()))
        
        server = self.server
        
        if "server" not in hkeys:
            self.outheaders.append(("Server", server.version))
        
        buf = [server.protocol, " ", self.status, "\r\n"]
        try:
            buf += [k + ": " + v + "\r\n" for k, v in self.outheaders]
        except TypeError:
            if not isinstance(k, str):
                raise TypeError("WSGI response header key %r is not a string.")
            if not isinstance(v, str):
                raise TypeError("WSGI response header value %r is not a string.")
            else:
                raise
        buf.append("\r\n")
        self.push("".join(buf))


class WSGIServer(asyncore.dispatcher):
    version = "Asynwsgi Server/1.0 alpha"
    protocol = "HTTP/1.1"
    def __init__(self, bind_addr, wsgi_app, server_name=None):
        asyncore.dispatcher.__init__ (self)
        self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
        self.set_reuse_addr()
        self.bind(bind_addr)
        self.listen(1024)
        self.keep_going = True
        self.wsgi_app = wsgi_app

        if not server_name:
            server_name = socket.gethostname()
        self.server_name = server_name

    def writable(self):
        return False

    def readable(self):
        return self.accepting

    def handle_accept(self):
        try:
            conn, addr = self.accept()
        except:
            return
        RequestHandler(conn, self)

    def reload(self, signum, frame):
        print 'reloading...'
        self.keep_going = False
        self.close()

    def stop(self, signum, frame):
        asyncore.close_all()

    def start(self):
        signal.signal(signal.SIGTERM, self.stop)
        signal.signal(signal.SIGINT, self.stop)
        asyncore.loop(timeout=0.1, use_poll=False)


if __name__ == "__main__":
    a = 0
    def application(environ, start_response):
        write = start_response('200 OK', [('content-type', 'text/html')])
        global a
        a += 1
        return ["<h1>Hello World!</h1><br />%s"%a]
    WSGIServer(('0.0.0.0', 8888), application).start()
